-
-
Notifications
You must be signed in to change notification settings - Fork 46
feat: Use the Handles, Luke! #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
I got the tests compiling and running. There are 9 failures with features 'lua' and 'rhai'. |
There is a regression in that it doesn't support multiple scripts. I'm trying to get that ready in the 'use-entities' branch, which is compiling now but not running yet if you wanted to take a peek. But maybe this PR needs to cook a little more. I'm gonna take another pass on it tonight. |
Ok, I have fixed the regression. Game of life works in all ways for Lua. For Rhai it doesn't seem to work for static scripts. So as I was trying to make scripts work with a shared context and per entity/script, I realized the split of contexts is actually a big policy decision. I imagine in most cases it'll come down to a few qualities: entity, script, and another thing I'm calling domain. To that I end I wrote this trait: /// A generic script context provider
pub trait ScriptContextProvider<P: IntoScriptPluginParams> {
/// Get the context.
fn get(&self, id: Option<Entity>, script_id: &ScriptId, domain: &Domain) -> Option<&Arc<Mutex<P::C>>>;
/// Insert a context.
fn insert(&mut self, id: Option<Entity>, script_id: &ScriptId, domain: &Domain, context: P::C) -> Result<(), P::C>;
/// Returns true if there is a context.
fn contains(&self, id: Option<Entity>, script_id: &ScriptId, domain: &Domain) -> bool;
} My hope is this would allow us to provide the right criteria to choose contexts divisions. SharedContextSo the shared context is implemented with this: pub struct SharedContext<P: IntoScriptPluginParams>(pub Option<Arc<Mutex<P::C>>>); and it ignores the arguments and just provides the same context to everyone. EntityContextA per entity context is implemented like this: /// Stores the script context by entity.
pub struct EntityContext<P: IntoScriptPluginParams>(HashMap<Entity, Arc<Mutex<P::C>>>); ScriptContext compositionAnd the resource we make available is a composition: #[derive(Resource)]
/// Keeps track of contexts
pub enum ScriptContext<P: IntoScriptPluginParams> {
/// One shared script context
Shared(SharedContext<P>),
/// One script context per entity
///
/// Stores context by entity with a shared context as a last resort when no
/// entity is provided.
Entity(EntityContext<P>, SharedContext<P>)
} And if we wanted to we could other provide other policies like by ScriptId or something. DomainBut what's domain? It's the old /// A kind of catch all type for script context selection
///
/// I believe this is what the original ScriptId was intended to be.
pub type Domain = Option<Cow<'static, str>>; I don't do anything with My vision for domain was to allow users to clump scripts together a little more haphazardly. Something like the following fantasy code would put scripts into one of two domains: asset_server.load_with_settings("player.lua", |settings: &mut ScriptSettings| {
settings.domain = Some("player".into());
});
asset_server.load_with_settings("monster.lua", |settings: &mut ScriptSettings| {
settings.domain = Some("env".into());
});
asset_server.load_with_settings("effects.lua", |settings: &mut ScriptSettings| {
settings.domain = Some("player".into());
}); Take awayThis code is working and I think it's ready for your attention. Having spent more time with this code base I'm in even more awe of what you've put together. Let me know if there's anything I can do to help. I'm sorry that this branch is probably going to conflict with 0.16 branch. :( Further workI'm going to try to make this branch work with Nano-9 to dog food it in the coming days. |
It was quick and painless to switch over to this branch in Nano-9. See the commit. |
DomainsI went ahead and fleshed out the pub type Domain = Cow<'static, str>; How to select a domain?I thought about adding a 'domain' field to /// Defines the domain of a script
#[derive(Component)]
pub struct ScriptDomain(pub Domain); Static scriptsI could allow for static scripts to have a domain but I have left them all as being in no domain. Are static scripts grouped into their own context? Are static scripts a kind of domain? Are they superseded by domains? Add support for context per scriptI added impl<P: IntoScriptPluginParams> ScriptContext<P> {
/// Use a shared script context
pub fn shared() -> Self {
Self::Shared(SharedContext::default())
}
/// Domain contexts or a shared context
pub fn with_domains() -> Self {
Self::Domain(DomainContext::default(), SharedContext::default())
}
/// Use one script context per entity
pub fn per_entity() -> Self {
Self::Entity(EntityContext::default(), SharedContext::default())
}
/// Use one script context per entity
pub fn per_script() -> Self {
Self::ScriptId(ScriptIdContext::default())
}
} Now that there are 4 choices, I'd suggest we change the app.insert_resource(ScriptContext::shared()); Problem: N scripts produce N callback evaluations even with 1 shared contextI added a Proposal: Evaluate Callbacks on a Per Context BasisHere is my take for least-surprise: If I have five scripts all in one shared context, and I fire a call to If I have four scripts in three different contexts, and I fire a call to To facilitate that, I added the following method: /// A generic script context provider
pub trait ScriptContextProvider<P: IntoScriptPluginParams> {
// ...
/// Hash for context.
///
/// Useful for tracking what context will be returned by `get()` without
/// requiring that `P::C` impl `Hash` and cheaper too.
fn hash(&self, id: Option<Entity>, script_id: &ScriptId, domain: &Option<Domain>) -> Option<u64>;
} What I think is nice about this is it works the same as before if you're placing every script in its own context. But it will also work with scripts, entities, domains, and shared contexts. Docs?Would you like some help updating the docs/book for this PR? |
I'm prepping this branch for a BMS 0.14.0. Expecting some real-world use I updated the
And its default is I found some tests failing (and some not even compiling--eek). I'll try to fix them tomorrow. |
I keep thinking I'm close to being done, but then cargo unveils some new slew of errors. After much teeth gnashing, I have all 172 tests passing when I run "cargo test" from the root workspace. However, when I go into the core crate's directory and run "cargo test" I get compilation errors from the tests. Shouldn't cargo run all the workspace crates' tests? Anyway, fixing the test harness unveiled this bug:
To fit better with I didn't seek out to do this, I think I was bewildered by the test harness not working, but as I was patching things up, I realized that we could load scripts without cloning their contents if they're given via a Will continue to work on this until I get all tests to pass. Do you have a magic command you provide on the command line to run all the tests? |
try |
Progress but |
It's all compiling now. 3 tests are failing. Unfortunately the fix seems a little hairy. The called_on_right_recipients test in particular requires a change in the event handler to fix. My workday has been interrupted quite a bit, so I put my partial commits into my "use-handles-dev" branch so it's not distracting in this PR. I changed the event handler and now 5 tests are failing. On it goes. |
All tests pass! The game of life example starts, stops with Lua & Rhai with and without static argument. Assumption BreakageI had been adding pub struct ContextKey {
/// Entity if there is one.
pub entity: Option<Entity>,
/// Script ID if there is one.
pub script_id: Option<Handle<ScriptAsset>>,
/// Domain if there is one.
pub domain: Option<Domain>,
} It's the new key to context. /// A generic script context provider
pub trait ScriptContextProvider<P: IntoScriptPluginParams> {
/// Get the context.
+ fn get(&self, context_key: &ContextKey) -> Option<&Arc<Mutex<P::C>>>;
- fn get(&self, id: Option<Entity>, script_id: &ScriptId, domain: &Option<Domain>) -> Option<&Arc<Mutex<P::C>>>;
// ...
} What's in a Domain?With the inclusion of /// A kind of catch all type for script context selection
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Domain(u64);
impl Domain {
/// Create a domain handle.
pub fn new(hashable: impl Hash) -> Self {
Domain(DefaultHashBuilder::default().hash_one(hashable))
}
} New is OldThis is the new default DomainEntityScriptId(Or<DomainContext<P>, Or<EntityScriptIdContext<P>, Or<ScriptIdContext<P>, SharedContext<P>>>>), You can read the
The prior version had behaviors 2 and 3 and 4 as a separate assignment mode. This adds domains without interfering with the rest if they're not used. And its easy to design different behaviors within the library. For instance, if we wanted to have all static scripts live in the same context we can drop the DomainX(Or<DomainContext<P>, Or<EntityScriptIdContext<P>, SharedContext<P>>>), This means Reify me notI did a big refactor on the It now has these benefits:
ApologiesI am sorry that this PR has gotten so big. At the beginning it seemed like this could be a less invasive set of changes. Having failed to make this change set small, I have endeavored to make it good. |
I updated the BMS book's chapter on contexts and managing scripts. I realized through the original documentation that I had misunderstood BMS's original script context isolation. I thought it somehow isolated scripts on an entity-script pair basis. I wrote the entity-script pair context so it would be a drop-in substitute for the original system. Oops. Turns out isolating based on script would be the drop-in replacement. Choose
|
Ello, apologies for not interacting much yet, rest assured I am observing the branch and trying to grok it (and I like this direction), I've now gotten the bevy migration branch to a final state, so now I am going to be reviewing the changes in this PR over the next week or so, do you mind merging the last bit of conflicts (as I need the xtask changes to load the workspace locally). for the merge order, now I am thinking:
thoughts? |
This is a combination of 68 commits. hack: It compiles. hack: Compile with lua54 feature. refactor: Accept Handle<ScriptAsset> in ScriptComponents. feature: Handle static scripts. bug: Fix rhea and static script unloading. feature: Add ScriptSetttings to loader. This introduces a serde dependency though. :( test: Prep tests to run. feature: Add DisplayProxy. Refactor continues. refactor: Bring test harness in line. test: Tests are running! test: Make test runnable without rhai. excise: Remove ScriptMetadata and ScriptEvent. We don't need them since asset's know their language. excise: Drop Script::asset field. It's a duplicate with Script::id now. chore: fix build problem in xtask perf: Can create_or_update_script w/o clone. feature: Try to use the entities. hack: It compiles but idk why. I fought with HandlerContext trying to add a Query to it for the longest time. hack: Compiles but doesn't run well. Log is flooded with these two lines: ```log 2025-06-28T11:46:54.321715Z ERROR bevy_mod_scripting_core::handler: Rhai: Failed to query entities with scripts: Cannot claim access to base type: Global. The base is already claimed by something else in a way which prevents safe access. Location: "/Users/shane/Projects/bevy_mod_scripting/crates/bevy_mod_scripting_core/src/extractors.rs:337". Context: Could not claim exclusive world access 2025-06-28T11:46:54.322046Z ERROR bevy_mod_scripting_core::handler: Lua: Failed to query entities with scripts: Cannot claim access to base type: Global. The base is already claimed by something else in a way which prevents safe access. Location: "/Users/shane/Projects/bevy_mod_scripting/crates/bevy_mod_scripting_core/src/extractors.rs:337". Context: Could not claim exclusive world access ``` feature: Use SharedContext(P::C) for global context. feature: GOL works with SharedContext. feature: Works with ScriptContextProvider trait. refactor: It's all coming together now. bug: Make per-entity work with static scripts. doc: Clarify EntityContext. Hide internal struct. chore: fix new clippy warnings causing build errors (makspll#428) partial: Add domain handling. feature: Add ScriptContextProvider::hash member. feature: Call labels on a per-context basis. doc: Explain call per-context rationale. feature: More ScriptComponentProvider<P> variants. chore: Restore commented log plugin. test: Remove script_id_generator parameter. test: Remove metadata tests. refactor: Prefer Option<Entity> vs Entity::from_raw(0). bug: Scripts w/o AssetServer didn't work. Now you can add scripts via `Assets<ScriptAsset>` or the `AssetServer`. test: Fix tests. perf: Avoid cloning script content when possible. doc: Document script::Or. refactor: Unscore unused parameters in context providers. chore: Fix it, Clippy! doc: Add note about ScriptContext::hash(). test: Fix/refactor tests. feature: Remove todo! & impl supported extensions. Implement the ConfigureScriptAssetSettings again. chore: Make core tests compile. test: It compiles! 8 test failures. test: All tests passing except 3. partial: Handle event rework. feature: Add iter() to ScriptContextProvider<P>. refactor: Rework event handling. partial: Collecting contexts. partial: More handler event mulling. feature: New event handler. Early exit. Fewer allocations. No querying unless necessary. feature: Add remove() to ScriptContextProvider<P>. feature: Make DeleteScript delete contexts too. feature: Add EntityScriptIdContext and ContextKey. feature: Rename iter() to values(). feature: Add iter() to ScriptContextProvider<P>. feature: Handle Recipient::ScriptId fully. Static scripts included. Fewer clones. refactor: Move ContextKey to its own file. feature: Made Domain(u64). refactor: Weave ContextKey in. It'd be nice if ContextKey had a Handle<ScriptAsset> just for logging purposes. refactor: Use ContextKey. Tests pass! refactor: Use ContextKey instead of ScriptId. chore: Clean up warnings. bug: Fix game of life. feature: Add prelude. I like explicit imports, but I'm trying to make the code in mdbook runnable, just so it is easier to maintain, and it feels like it'd be a mess without a prelude. doc: Update book. A few code refactors to make book writing easier. feature: Add ConfigureScriptAssetSettings to prelude.
Great to hear! I think the versions sound fine. A handles+bevy0.15 as an alpha is good by me, better even than polluting the version space with use-handles and not.
I think the desired end is good, but I worry the process of rebasing this branch onto a 0.16 will be a chore. But maybe it won't. To update this branch to "main," I'm going to squash everything in this branch just to make the rebasing easier. Maybe it won't be as troublesome if the commits are fewer, but I was initially thinking I'd take the patch from the old handles+0.16 and apply that on top of this branch with whatever fix up was required. |
Reload and unload don't seem to be working yet.
I squashed this branch and rebased it to main. It's compiling. There are two tests which are failing.
And I'm not seeing the unload and reload work yet. Probably need to add a test for those. |
Ah yeah sorry by rebase I mean rebase OR merge, don't mind which, squash and rebase is also good! Folks at work treat those words interchangably and it's infected me |
Nice, I will pull this down locally and play with it |
Fixed the failing assignment tests. All tests are passing! I added an example called "run-script" to exercise the reload functionality. It's a mouthful to run reload.lua because of the needed features: cargo run --features lua54,bevy/file_watcher,bevy/multi_threaded --example run-script -- reload.lua I know you're looking to get more examples, so I figured this might be good to have for the rust part and maybe putting together quick pure lua or rhai examples as well. Reload functionality is not working yet in this branch. |
This error persists for a variable `output` which I DO NOT SEE in the code it references. ``` warning: unused variable: `output` --> crates/bevy_mod_scripting_functions/src/core.rs:419:8 | 419 | fn add_system( | ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_output` | = note: `#[warn(unused_variables)]` on by default ```
One clippy warning remains but I don't know why. It refers to an "output" variable that isn't there. See last commit for the exact log. |
🚧 WIP: Review in progress🚧 Alright, the PR changes quite a few things I am going to use the following convention in my reviews:
And do keep in my mind I am happy to make those changes, so do let me know if you want me to! I'll also divide my thoughts into the areas that were touched: 🛑/🤷 Script asset events directly tied to script asset handler systemsThis was previously decoupled via proxy script events for two reasons:
I believe this was removed in this PR since the metadata synchronising is no longer necessary, but for the sake of 1. I'd personally still see value in the decoupling, maybe via: pub(crate) enum ScriptAssetEventKind {
Added,
Attached,
Detached,
Removed,
Modified,
} and a matching struct with the payload, or repeated enum constructors whatever's easier. It seems we also do not handle EDIT: I see we do not ever do this via assets anymore, which makes sense, I think pulling out script events into their own thing, and then dispatching those events from the new component changes source (i.e. Added) would be neat 🛑 Script evaluation queue feels unnecessaryRelates to my first point, if say we coalesced all script events into our "Script Event" dimension first, and then populated those events from the new sources (i.e. Added script component = ScriptEvent::Attached), we could handle everything in a pretty unified way. And in this area, the queue is used to collect all these script related events (currently, ScriptAdded, and the static script equivalent), and then immediately consume it. I'd like to see: handle script events: ScriptEvent -> Script Commands Not sure about the exact event details though, added vs attached etc, Do we need new commands? Attached Vs Detached ? 🤷 ScriptContexts enum is very complexWhat we're doing is mapping:
by embedding this strategy in the enum as is we're implying that to read any of the contexts, you have to do this differently per policy we are using. My immediate thought would be to have the context store as a completely separate concept, i.e. a The |
Thanks for taking the time to look over it. Script asset events
That was the reason, but I think you're right that Script evaluation queue feels unnecessaryIt started as a system I realized too that the queue isn't necessary unless you're putting scripts into the same context. I will attempt to do something as you suggest but I admit I'm foggy on what that will entail. ScriptContext enum is very complexThat's a really good point. It's perhaps a labor of understanding that we can now schluff off, but I think you're right. We could get the same effect by having a fn shared(context_key: ContextKey) -> ContextKey {
ContextKey::default()
}
fn per_script(context_key: ContextKey) -> ContextKey {
ContextKey {
script: context_key.script,
..default()
}
} Oh, I see perhaps you'd suggest I kind of liked how script handles were a proxy for context handles—when all script handles were dropped, the context was dropped—and it has made me wonder if Contexts ought to be assets with their own handles. [Shrug.]
Hmm, I don't quite follow but I'm intrigued by the idea. What's the type of
I hadn't thought domains would be related to asset paths. Best example I have for their potential use is what I used in the book example: commands.spawn((
ScriptComponent(vec![asset_server.load("player.lua")]),
ScriptDomain(Domain::new("player")),
)).with_children(|parent| {
parent.spawn((
ScriptComponent(vec![asset_server.load("sword.lua")]),
ScriptDomain(Domain::new("player")),
));
}) |
Cannot call scripts within a global block. Had to allocate instead and return the vector.
Drop all those modules. Drop the trait. Use a simple enum `ContextRule` that allows for custom rules. User can now configure policies more flexibly.
I completely reworked the ScriptContext, and I think it's much better now. I dropped all those ScriptContextProvider modules. I dropped the ScriptContextProvider trait. Took out a lot of lines of code. Yay! We use a simple enum I've reintroduced the I haven't sorted out the tests yet.
|
All right. This is hopefully looking more like what you asked.
Also since we're storing |
Looking really good! The events make this extremely easy to read! |
Some minor code changes for easier book writing.
Starting with this branch, I re-applied Alex Parlett's 0.16 patch which is now on available on use-handles-0.16. |
I am just working out the last few kinks, namely:
|
Re: script residency. [Looks at the test harness lib.rs:305 file.] Oh, that's right. I imagine you're looking at this code: if let Some(event) = error_events.into_iter().next() {
// eprintln!("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx");
// if ! app.world().resource::<AssetServer>().load_state(&handle).is_loaded() {
// continue;
// }
return Err(event
.error
.display_with_world(WorldGuard::new_exclusive(app.world_mut())));
} I commented that out, but the script is being added a few lines above lib.rs:244. // The following code did not work, possibly because of the asynchronous
// nature of AssetServer.
//
// ```
// let handle = app.world_mut().resource_mut::<AssetServer>().load(&script_path);
// app.world_mut().spawn(ScriptComponent::new([handle.clone()]));
// ```
let handle = {
let mut script_dir = manifest_dir.clone();
script_dir.push("assets");
script_dir.push(script_id.path());
// Read the contents and don't do anything async.
let content = fs::read_to_string(&script_dir)
.map_err(|io| format!("io error {io} for path {script_dir:?}"))?;
let mut script = ScriptAsset::from(content);
script.language = P::LANGUAGE;
app.world_mut()
.resource_mut::<Assets<ScriptAsset>>()
.add(script)
}; I was getting intermittent behavior if I recall correctly, so I figured if I could make script loading synchronous rather than async, it would be more reliable. |
oh yeah I get that, in the integration test though I'd ideally like to run the pipeline from start to end, for unit tests that makes a lot of sense. I haven't quite gotten there this weekend as I'd hoped, I've bumped into a few more things I haven't noticed before (apologies, don't worry about patching to 0.16, I can continue from here), still have to cook a little longer, but the current TL;DR of what I am working on is:
Haven't said it enough yet, but your work is much appreciated, and got me thinking a lot! Had I been implementing this from the start I would probably involve auto-incrementing u64's for context ID's and lots more bookkeeping, so it's great to get a new perspective! I am still thinking about your let me know if there's anything concerning in the above list, or if I missed something! |
Thanks! I appreciate the kind words. I'm happy to have you take the reigns. I felt like I had been pretty intrusive with my changes, so I tried not to upset the apple cart where I could, but I'm glad you're reconsidering some things like The only things I'd want preserved are handles for scripts, in-order loading (but it could be relaxed to be in-order per context), and a notion of domains. (I realized with Nano-9 if I use a "nano-9" domain instead of a shared context then I wouldn't have to monopolize BMS, and the user would still be free to use BMS in any fashion they like, which feels like a win.) I look forward to seeing your changes. Happy to test and review when it's ready. |
I was only planning to file issue #426 tonight, which states the problems I was having. I thought if I poked around I'd find the implementation reason for why what I suggest there wouldn't work. So I made a branch and rolled up my sleeves, and I was able to pretty quickly get exactly what I wanted.
Handles
In the issue I suggest doing away with
ScriptId
. As I worked with the implementation, it became clear I actually wanted to redefine it.Beyond that I made it so
ScriptComponent
holdsHandle<ScriptAsset>
s. The nice thing about this is you can use this pattern:No need to store a strong handle somewhere so your script isn't unloaded. However, if you rely on that behavior you can still do that like you do with any other asset-based item using weak handles.
This preserves the semantics of the current release.
ScriptAsset loading versus evaluation
In this PR the static scripts are
handled essentially the same as before except theycan retain handles (both strong and weak). EDIT: A script may be loaded without evaluation. If it is added as a static script, it will then be evaluated.The non-static scripts are not evaluated until they are added to a
ScriptComponent
. Then they are evaluated in order, sequentially.ScriptAsset change
I suggested a change to
ScriptAsset
and that survived this refactoring.I also provided a
ScriptSettings
where one can provide a definite language.None
will use the file's extension.One downside about
ScriptSettings
is it required adding "serde" with the "deriv" feature as a dependency.Example
I updated the Game of Life to work with Lua and Rhai and all its console options.
Remaining work
I have not tried to exercise any other languages or even do much with the tests. But I'd be happy to move this forward to handle the rest with some consensus that that's the right direction.